/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.business;

import TimeRecording.enums.MonthStatus;
import TimeRecording.tables.AllocationOfHours;
import TimeRecording.tables.Employee;
import TimeRecording.tables.MonthlyControl;
import TimeRecording.tables.MonthlyTime;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import javax.persistence.EntityManager;

/**
 *
 * @author dpatchett
 */
public class MonthEndCalculations extends PeriodCalculations {

    /**
     * Instantiates a new "period calculator" for the given employee/date-range
     * and loads up all of the required data.
     *
     * @param entityManager
     * @param employee
     * @param dateRange
     */
    public MonthEndCalculations(
            EntityManager entityManager,
            Employee employee,
            DateRange dateRange) {

        super(entityManager, employee, dateRange);
    }

    /**
     * This is a general "refresh data" type method that can be invoked by the
     * user when the want to just freshen up the month end data.
     *
     * @param entityManager
     */
    public static void processMonthEnd(EntityManager entityManager) {

        MonthlyControl thisMonthControl =
                MonthlyControl.getLatest(entityManager);

        if (thisMonthControl != null) {
            processMonthEnd(entityManager,
                    thisMonthControl.getStartDate(),
                    thisMonthControl.getEndDate(),
                    thisMonthControl.getSessionalStartDate(),
                    thisMonthControl.getSessionalEndDate());
        }
    }

    /**
     * This method will perform the month end processing for a specific pair of
     * date ranges.  One date range specifies the payment period for salaried
     * working; the other specifies the date range for sessional working.
     *
     * @param entityManager
     * @param startDate
     * @param endDate
     * @param sessionalStartDate
     * @param sessionalEndDate
     */
    public static void processMonthEnd(EntityManager entityManager,
            Date startDate, Date endDate,
            Date sessionalStartDate, Date sessionalEndDate) {

        // Get the date ranges for this month and the previous 2 months

        MonthlyControl thisMonthControl = null;
        MonthlyControl lastMonthControl = null;
        MonthlyControl monthBeforeLastControl = null;
        GregorianCalendar cal = new GregorianCalendar();

        // Get this month's control record (if it's there); if not then create one
        thisMonthControl = MonthlyControl.get(entityManager, startDate, true);
        if (thisMonthControl == null) {
            thisMonthControl = new MonthlyControl(
                    startDate, endDate,
                    sessionalStartDate, sessionalEndDate);
        }

        // Adjust the end-dates as per the user-entered values
        thisMonthControl.setEndDate(endDate);
        thisMonthControl.setSessionalEndDate(sessionalEndDate);

        entityManager.persist(thisMonthControl);

        // Get last month's control record (if it's there)
        cal.setTime(thisMonthControl.getStartDate());
        cal.add(GregorianCalendar.DATE, -1);
        lastMonthControl = MonthlyControl.get(
                entityManager, cal.getTime(), false);

        // If we were able to get last month's control record
        // then try to get the previous month's too
        if (lastMonthControl != null) {
            cal.setTime(lastMonthControl.getStartDate());
            cal.add(GregorianCalendar.DATE, -1);
            monthBeforeLastControl = MonthlyControl.get(
                    entityManager, cal.getTime(), false);
        }

        // Step 1: reassess everything that's not set in stone

        GregorianCalendar expandedStartDate = new GregorianCalendar();
        expandedStartDate.setTime(startDate);
        expandedStartDate.add(GregorianCalendar.MONTH, -2);

        GregorianCalendar expandedEndDate = new GregorianCalendar();
        expandedEndDate.setTime(endDate);
        expandedEndDate.add(GregorianCalendar.MONTH, 2);

        reassessAndPersistWorkingDays(entityManager, new DateRange(
                expandedStartDate.getTime(),
                expandedEndDate.getTime()));

        // Step 2: on a "by employee" basis, update the
        // previous month end and the current month end

        List<Employee> employees = Employee.getActiveEmployees(entityManager);

        MonthlyTime monthBeforeLast = null;
        MonthlyTime lastMonth = null;

        for (Employee employee : employees) {

            // Start by getting the "latest" figures from our oldest monthly period

            if (monthBeforeLastControl != null) {
                monthBeforeLast = MonthlyTime.get(entityManager,
                        monthBeforeLastControl.getStartDate(), employee);
            } else {
                monthBeforeLast = null;
            }

            // Update the revised figures for last month taking into account
            // any revised figures from our oldest month end record

            if (lastMonthControl != null) {
                if (monthBeforeLast != null) {
                    lastMonth = processMonthEnd(
                            entityManager, employee, lastMonthControl,
                            monthBeforeLast.getRevisedAllocation(), false);
                } else {
                    lastMonth = processMonthEnd(entityManager, employee,
                            lastMonthControl, null, false);
                }
            }

            // Update the original AND revised figures for this month

            if (lastMonth != null) {
                processMonthEnd(entityManager, employee, thisMonthControl,
                        lastMonth.getRevisedAllocation(), true);

                processMonthEnd(entityManager, employee, thisMonthControl,
                        lastMonth.getRevisedAllocation(), false);
            } else {
                processMonthEnd(entityManager, employee,
                        thisMonthControl, null, true);

                processMonthEnd(entityManager, employee,
                        thisMonthControl, null, false);
            }
        }

        if (lastMonthControl != null &&
                lastMonthControl.getStatus() == MonthStatus.PART_PROCESSED) {
            lastMonthControl.setStatus(MonthStatus.FULLY_PROCESSED);
            entityManager.merge(lastMonthControl);
        }

        if (thisMonthControl != null &&
                thisMonthControl.getStatus() == MonthStatus.IN_PROGRESS) {
            thisMonthControl.setStatus(MonthStatus.PART_PROCESSED);
            entityManager.merge(thisMonthControl);
        }
    }

    /**
     * This method is responsible for calculating the month end figures for a
     * given employee.  It works out details for sessional and non-sessional
     * hours based on different date/times. and then amalgamates the figures
     * into a single month-end record.  Where appropriate, adjustments are
     * carried through based on discrepancies from previous periods.
     *
     * Important: the month end being processed might not necessarily be the
     * current/most recent.
     *
     * @param entityManager
     * @param employee
     * @param monthControl
     * @param offsets
     * @param original
     * @return
     */
    private static MonthlyTime processMonthEnd(EntityManager entityManager,
            Employee employee, MonthlyControl monthControl,
            AllocationOfHours offsets, boolean original) {

        // Get the rolling figures for this month (they shouldn't be
        // there yet but if they are then we ought to tolerate them)

        MonthlyTime monthlyTime = MonthlyTime.get(entityManager,
                monthControl.getStartDate(), employee);

        if (monthlyTime == null) {
            monthlyTime = new MonthlyTime(
                    employee.getEmployeeId(),
                    monthControl.getStartDate());
            monthlyTime.setEndDate(monthControl.getEndDate());
            monthlyTime.setSessionalStartDate(monthlyTime.getSessionalStartDate());
            monthlyTime.setSessionalEndDate(monthlyTime.getSessionalEndDate());
        }

        // Get the totals for the "salaried" period
        MonthEndCalculations salariedCalcs = new MonthEndCalculations(
                entityManager, employee, new DateRange(
                monthControl.getStartDate(),
                monthControl.getEndDate()));

        salariedCalcs.reassessWorkingDays(entityManager);
        AllocationOfHours salariedTotals = salariedCalcs.getTotals();

        // Get the totals for the "sessional" period
        // (note the use of a different date range)
        MonthEndCalculations sessionalCalcs = new MonthEndCalculations(
                entityManager, employee, new DateRange(
                monthControl.getSessionalStartDate(),
                monthControl.getSessionalEndDate()));

        AllocationOfHours sessionalTotals = sessionalCalcs.getTotals();

        // Transfer the sessional figures into the salaried totals
        salariedTotals.setSessionExpected(sessionalTotals.getSessionExpected());
        salariedTotals.setSessionHoliday(sessionalTotals.getSessionHoliday());
        salariedTotals.setSessionSickness(sessionalTotals.getSessionSickness());
        salariedTotals.setSessionRecorded(sessionalTotals.getSessionRecorded());

        // Add in any offsets (adjustments from previous months)
        if (offsets != null) {
            salariedTotals.add(offsets);
        }

        // Update the correct slot (revised or original)
        if (original) {
            monthlyTime.setAllocation(salariedTotals);
        } else {
            monthlyTime.setRevisedAllocation(salariedTotals);
        }

        // Persist our changes
        entityManager.persist(monthlyTime);

        return monthlyTime;
    }
}